Signal Decomposition Using EVD of Hankel Matrices

Suppose we are given a data signal which consists of several nearly mono-components.

Can we recover the mono-components?

The answer is YES, with an efficient algorithm using EVDs of Hankel matrices.

Mono-component recovery can be successfully applied to audio signals.

Prerequisites

The reader should be familiar to elementary concepts about signals, and with linear algebra concepts, particularly EVD and its properties and algorithms.

Competences

The reader should be able to decompose given signal into mono-components using EVD methods.

References

For more details see P. Jain and R. B. Pachori, An iterative approach for decomposition of multi-component non-stationary signals based on eigenvalue decomposition of the Hankel matrix.

Credits: The first Julia implementation was derived in A. M. Bačak, Master's Thesis.

Extraction of stationary mono-components

Definitions

Let $x\in\mathbb{R}^{m}$, denote a signal with $N$ samples.

Assume $x$ is composed of $L$ stationary mono-components:

$$ x=\sum\limits_{k=1}^L x^{(k)}, $$

where

$$ x^{(k)}_i=A_k \cos(2\pi f_k i +\theta_k)\quad i=1,2,\ldots,m. $$

Here:

$f_k=\displaystyle\frac{F_k}{F}$ is the normalized frequency of $x^{(k)}$,

$F$ is the sampling frequency of $x$ in Hz,

$F_k$ is the sampling frequency of $x^{(k)}$,

$A_k$ is the amplitude of $x^{(k)}$, and

$\theta_k$ is the phase of $x^{(k)}$.

We assume that $F_k< F_{k+1}$ for $k=1,2,\ldots,n-1$, and $F>2F_n$.

A Hankel matrix is a (real) square matrix with constant values along the skew-diagonals. More precisely, let $a\in\mathbb{R}^{2n-1}$. An $n\times n$ matrix $H\equiv H(a)$ for which $H_{ij}=H_{i+1,j-1}=a_{i+j-1}$ is a Hankel matrix.

Facts

Let $x$ be a signal with $2n-1$ samples composed of $L$ stationary mono-components.

Let $H$ be an $n\times n$ Hankel matrix corresponding to $x$ and let $H=U\Lambda U^T$ be its EVD (Hankel matrix is symmetric) with $\lambda_1\leq \lambda_2 \leq \cdots \leq \lambda_n$.

Smilarly, let $H_k$ be the $n\times n$ Hankel matrix corresponding to the $k$-th component $x^{(k)}$ and let $H_k=U_k\Lambda_k U_k^T$ be its EVD.

  1. $H=\sum\limits_{k=1}^{L} H_k$.

  2. $H_k=\lambda_k U_{:,k}U_{:,k}^T + \lambda_{n-k+1} U_{:,n-k+1}U_{:,n-k+1}^T$.

Example - Signal with three mono-components


In [1]:
using Plots
using SpecialMatrices

In [2]:
# Small Hankel matrix
a=collect(1:11)
Hankel(a)


Out[2]:
6×6 Hankel{Int64}:
 1  2  3  4   5   6
 2  3  4  5   6   7
 3  4  5  6   7   8
 4  5  6  7   8   9
 5  6  7  8   9  10
 6  7  8  9  10  11

In [3]:
# Create the signal
n=160
N=2*n-1
F = 6400
L = 3
A = [3, 2, 1]
Fk= [200, 320, 160]
θ = [pi/2, pi/4, 0]
x = zeros(N)
for k=1:L
    for i=1:N
        x[i]+=A[k]*cos(2*pi*Fk[k]*i/F+θ[k])
    end
end
plot(x,xlabel="Number of samples N", ylabel="Amplitude")


Out[3]:
0 100 200 300 -5.0 -2.5 0.0 2.5 5.0 Number of samples N Amplitude y1

In [4]:
# FFT indicates that there are three components
# The plot of abs.(y[1:end]) is symmetric arround midpoint 
using FFTW
y=fft(x)
plot(log10.(abs.(y))[1:100])


Out[4]:
0 25 50 75 100 0.0 0.5 1.0 1.5 2.0 2.5 y1

In [5]:
# Let us decompose the signal 
H=Hankel(x)


Out[5]:
160×160 Hankel{Float64}:
  1.3104     0.115875  -1.08857   …   4.07448    3.35497    2.41421 
  0.115875  -1.08857   -2.22028       3.35497    2.41421    1.3104  
 -1.08857   -2.22028   -3.20152       2.41421    1.3104     0.115875
 -2.22028   -3.20152   -3.96587       1.3104     0.115875  -1.08857 
 -3.20152   -3.96587   -4.46374       0.115875  -1.08857   -2.22028 
 -3.96587   -4.46374   -4.66636   …  -1.08857   -2.22028   -3.20152 
 -4.46374   -4.66636   -4.56793      -2.22028   -3.20152   -3.96587 
 -4.66636   -4.56793   -4.18585      -3.20152   -3.96587   -4.46374 
 -4.56793   -4.18585   -3.55882      -3.96587   -4.46374   -4.66636 
 -4.18585   -3.55882   -2.74321      -4.46374   -4.66636   -4.56793 
 -3.55882   -2.74321   -1.80783   …  -4.66636   -4.56793   -4.18585 
 -2.74321   -1.80783   -0.827855     -4.56793   -4.18585   -3.55882 
 -1.80783   -0.827855   0.121836     -4.18585   -3.55882   -2.74321 
  ⋮                               ⋱                                 
  0.555961   1.35743    2.19081      -1.22175   -0.762656  -0.163073
  1.35743    2.19081    2.99615      -0.762656  -0.163073   0.555961
  2.19081    2.99615    3.70922   …  -0.163073   0.555961   1.35743 
  2.99615    3.70922    4.2674        0.555961   1.35743    2.19081 
  3.70922    4.2674     4.61573       1.35743    2.19081    2.99615 
  4.2674     4.61573    4.71235       2.19081    2.99615    3.70922 
  4.61573    4.71235    4.53309       2.99615    3.70922    4.2674  
  4.71235    4.53309    4.07448   …   3.70922    4.2674     4.61573 
  4.53309    4.07448    3.35497       4.2674     4.61573    4.71235 
  4.07448    3.35497    2.41421       4.61573    4.71235    4.53309 
  3.35497    2.41421    1.3104        4.71235    4.53309    4.07448 
  2.41421    1.3104     0.115875      4.53309    4.07448    3.35497 

In [6]:
using LinearAlgebra
λ,U=eigen(Matrix(H))
λ


Out[6]:
160-element Array{Float64,1}:
 -239.9999999999991       
 -160.0000000000001       
  -79.99999999999949      
   -6.954988337803782e-13 
   -5.3290705182007514e-14
   -3.907186464613127e-14 
   -2.9788712322638554e-14
   -2.683129082722271e-14 
   -2.5986385299720367e-14
   -2.4939524503263148e-14
   -2.4424906541753444e-14
   -2.1842051486212786e-14
   -2.1760371282653068e-14
    ⋮                     
    2.205264994553126e-14 
    2.2091712020148427e-14
    2.5564532223176637e-14
    2.990786304165372e-14 
    3.019806626980426e-14 
    3.126737327296269e-14 
    3.4724609786192e-14   
    5.0182080713057076e-14
    7.875057712690488e-14 
   80.00000000000014      
  160.00000000000014      
  240.0                   

We see that the three smallest and the three largest eigenvalues come in pairs and define the three mono-components.

The ratios of the moduli of the eigenvalues correspond to the ratios of the amplitudes of the mono-components.


In [7]:
# Form the three matrices
Hcomp=Array{Any}(undef,3)
for k=1:L
    Hcomp[k]=λ[k]*U[:,k]*U[:,k]' + λ[end-k+1]*U[:,end-k+1]*U[:,end-k+1]'
end

In [8]:
# Compare the first matrix with the Hankel matrix of the first mono-component
x1 = zeros(N)
l=1
for i=1:N
    x1[i]+=A[l]*cos(2*pi*Fk[l]*i/F+θ[l])
end

In [9]:
H1=Hankel(x1)
eigvals(Matrix(H1)), norm(Hcomp[1]-H1)


Out[9]:
([-240.0, -1.85481e-13, -1.79358e-13, -1.66881e-13, -1.64149e-13, -1.59941e-13, -1.37728e-13, -1.32831e-13, -1.16649e-13, -1.15799e-13  …  1.13143e-13, 1.17539e-13, 1.2208e-13, 1.37332e-13, 1.5802e-13, 1.60267e-13, 1.69526e-13, 1.79724e-13, 1.83003e-13, 240.0], 1.6680439311773043e-12)

In [11]:
# Now we reconstruct the mono-components from the skew-diagonal elements of Hcomp
xcomp=Array{Array{Float64}}(undef,L)
z=Array{Float64}(undef,N)
for k=1:L
    z[1:2:N]=diag(Hcomp[k])
    z[2:2:N]=diag(Hcomp[k],1)
    xcomp[k]=copy(z)
end

In [12]:
xaxis=collect(1:N)
plot([xcomp[1],xcomp[2],xcomp[3]])


Out[12]:
0 100 200 300 -3 -2 -1 0 1 2 3 y1 y2 y3

Fast EVD of Hankel matrices

Several outer eigenvalues pairs of Hankel matrices can be computed using Lanczos method. If the multiplication $Hx$ is performed using Fast Fourier Transform, this EVD computation is very fast.

Definitions

A Toeplitz matrix is a (real) square matrix with constant values along the diagonals. More precisely, let

$$ a=(a_{-(n-1)},a_{-(n-2)},\ldots,a_{-1},a_0,a_1,\ldots,a_{n-1})\in\mathbb{R}^{2n-1}. $$

An $n\times n$ matrix $T\equiv T(a)$ for which $T_{ij}=T_{i+1,j+1}=a_{i-j}$ is a Toeplitz matrix.

A circulant matrix is a Toeplitz matrix where each column is rotated one element downwards relative to preceeding column.

More precisely, let $a\in\mathbb{R}^{n}$. An $n\times n$ matrix $C\equiv C(a)=T(a,a_{1:n-1})$ is a Circulant matrix.

A rotation matrix is an identity matrix rotated 90 degrees to the right (or left).

A Fourier matrix is Vandermonde matrix

$$ F_n=V(1,\omega_n,\omega_n^2,\ldots, \omega_n^{n-1}), $$

where $\omega_n=exp(2\pi i/n)$ is the $n$-th root of unity (see the notebook Eigenvalue Decomposition - Definitions and Facts).

Example

Notice different meaning of vector $a$: in C=Circulant(a), $a$ is the first column, in T=Toeplitz(a), $a_i$ is the diagonal element of the $i$-th diagonal starting from $T_{1n}$, and in H=Hankel(a), $a_i$ is the element of the $i$-th skew-diagonal starting from $H_{11}$.


In [13]:
C=Circulant([1,2,3,4,5])


Out[13]:
5×5 Circulant{Int64}:
 1  5  4  3  2
 2  1  5  4  3
 3  2  1  5  4
 4  3  2  1  5
 5  4  3  2  1

In [14]:
TC=Toeplitz([2,3,4,5,1,2,3,4,5])


Out[14]:
5×5 Toeplitz{Int64}:
 1  5  4  3  2
 2  1  5  4  3
 3  2  1  5  4
 4  3  2  1  5
 5  4  3  2  1

In [15]:
T=Toeplitz([1,2,3,4,5,6,7,8,9])


Out[15]:
5×5 Toeplitz{Int64}:
 5  4  3  2  1
 6  5  4  3  2
 7  6  5  4  3
 8  7  6  5  4
 9  8  7  6  5

In [16]:
H1=Hankel([1,2,3,4,5,6,7,8,9])


Out[16]:
5×5 Hankel{Int64}:
 1  2  3  4  5
 2  3  4  5  6
 3  4  5  6  7
 4  5  6  7  8
 5  6  7  8  9

In [18]:
Vandermonde([6,2,3,4,5])


Out[18]:
5×5 Vandermonde{Int64}:
 1  6  36  216  1296
 1  2   4    8    16
 1  3   9   27    81
 1  4  16   64   256
 1  5  25  125   625

Facts

For more details see G. H. Golub and C. F. Van Loan, Matrix Computations, p. 202, and the references therein

  1. Hankel matrix is the product of a Toeplitz matrix and the rotation matrix.

  2. Circulant matrix is normal and, thus, unitarily diagonalizable, with the eigenvalue decomposition $$ C(a)=U\mathop{\mathrm{diag}}(F_n^* a)U^*, $$ where $U=\displaystyle\frac{1}{\sqrt{n}} F_n$. The product $F_n^* a$ can be computed by the Fast Fourier Transform(FFT).

  3. Given $a,x\in\mathbb{R}^n$, the product $y=C(a)x$ can be computed using FFT as follows: \begin{align*} \tilde x&=F_n^*x\\ \tilde a&=F_n^*a\\ \tilde y&=\tilde x.* \tilde a\\ y&= F_n^{-*} \tilde y. \end{align*}

  4. Toeplitz matrix of order $n$ can be embedded in a circulant matrix of order $2n-1$: if $a\in\mathbb{R}^{2n-1}$, then $$ T(a)=[C([a_{n+1:2n-1};a_{1:n}])]_{1:n,1:n}. $$

  5. Further, let $x\in\mathbb{R}^{n}$ and let $\bar x\in\mathbb{R}^{2n-1}$ be equal to $x$ padded with $n-1$ zeros.Then $$ T(a)x=[C([a_{n+1:2n-1};a_{1:n}])\bar x]_{1:n}. $$

  6. Fact 1 implies $H(a)x=(T(a)J)x=T(a)(Jx)$.

Examples


In [35]:
# Fact 1
eye(n)=Matrix{Int64}(I,n,n)
J=rotl90(eye(5))
T*J


Out[35]:
5×5 Array{Int64,2}:
 1  2  3  4  5
 2  3  4  5  6
 3  4  5  6  7
 4  5  6  7  8
 5  6  7  8  9

In [36]:
rotl90(T)


Out[36]:
5×5 Array{Int64,2}:
 1  2  3  4  5
 2  3  4  5  6
 3  4  5  6  7
 4  5  6  7  8
 5  6  7  8  9

In [37]:
# Fact 1
Matrix(T)*J


Out[37]:
5×5 Array{Int64,2}:
 1  2  3  4  5
 2  3  4  5  6
 3  4  5  6  7
 4  5  6  7  8
 5  6  7  8  9

In [38]:
# Fact 2
import Random
Random.seed!(467)
a=rand(-8:8,6)
n=length(a)
C=Circulant(a)
ω=exp(2*pi*im/n)
v=[ω^k for k=0:n-1]
F=Vandermonde(v)
U=F/sqrt(n)
λ=Matrix(F)'*a


Out[38]:
6-element Array{Complex{Float64},1}:
                 1.0 + 0.0im                   
 -15.000000000000002 + 3.4641016151377513im    
  -7.999999999999998 + 1.7320508075688732im    
   9.000000000000004 + 1.7486012637846216e-14im
   -8.00000000000001 - 1.7320508075688812im    
 -14.999999999999993 - 3.464101615137774im     

In [39]:
fft(a)


Out[39]:
6-element Array{Complex{Float64},1}:
   1.0 + 0.0im               
 -15.0 + 3.4641016151377544im
  -8.0 + 1.7320508075688772im
   9.0 + 0.0im               
  -8.0 - 1.7320508075688772im
 -15.0 - 3.4641016151377544im

In [40]:
v


Out[40]:
6-element Array{Complex{Float64},1}:
                 1.0 + 0.0im                  
  0.5000000000000001 + 0.8660254037844386im   
 -0.4999999999999998 + 0.8660254037844388im   
                -1.0 + 3.885780586188048e-16im
 -0.5000000000000006 - 0.8660254037844385im   
 0.49999999999999944 - 0.8660254037844392im   

In [41]:
F


Out[41]:
6×6 Vandermonde{Complex{Float64}}:
 1.0+0.0im   1.0+0.0im           1.0+0.0im          …   1.0+0.0im        
 1.0+0.0im   0.5+0.866025im     -0.5+0.866025im         0.5-0.866025im   
 1.0+0.0im  -0.5+0.866025im     -0.5-0.866025im        -0.5-0.866025im   
 1.0+0.0im  -1.0+3.88578e-16im   1.0-7.77156e-16im     -1.0+1.94289e-15im
 1.0+0.0im  -0.5-0.866025im     -0.5+0.866025im        -0.5+0.866025im   
 1.0+0.0im   0.5-0.866025im     -0.5-0.866025im     …   0.5+0.866025im   

In [42]:
C


Out[42]:
6×6 Circulant{Int64}:
 -6  -1   6   1   5  -4
 -4  -6  -1   6   1   5
  5  -4  -6  -1   6   1
  1   5  -4  -6  -1   6
  6   1   5  -4  -6  -1
 -1   6   1   5  -4  -6

In [43]:
eigvals(Matrix(C))


Out[43]:
6-element Array{Complex{Float64},1}:
                9.0 + 0.0im               
 0.9999999999999999 + 0.0im               
 -14.99999999999999 + 3.464101615137751im 
 -14.99999999999999 - 3.464101615137751im 
               -8.0 + 1.7320508075688776im
               -8.0 - 1.7320508075688776im

In [44]:
fft(a)


Out[44]:
6-element Array{Complex{Float64},1}:
   1.0 + 0.0im               
 -15.0 + 3.4641016151377544im
  -8.0 + 1.7320508075688772im
   9.0 + 0.0im               
  -8.0 - 1.7320508075688772im
 -15.0 - 3.4641016151377544im

In [45]:
# Residual
norm(Matrix(C)*U-U*Diagonal(λ))


Out[45]:
4.129481218738273e-14

In [46]:
?fft;

In [47]:
# Check fft
norm(λ-fft(a))


Out[47]:
3.020260682225245e-14

Fact 3 - Circulant() x vector, as implemented in the package SpecialMatrices.jl

function *(C::Circulant{T},x::Vector{T}) where T
    xt=fft(x)
    vt=fft(C.c)
    yt=vt.*xt
    real(ifft(yt))
end

Similarly, mul!()

function mul!(y::StridedVector{T},C::Circulant{T},x::StridedVector{T}) where T
    xt=fft(x)
    vt=fft(C.c)
    yt=ifft(vt.*xt)
    if T<: Int
        map!(round,y,yt) 
    elseif T<: Real
        map!(real,y,yt)
    else
        copy!(y,yt)
    end
    return y
end

In [48]:
x=rand(-9:9,n)


Out[48]:
6-element Array{Int64,1}:
 -6
  1
 -9
 -4
 -7
  9

In [49]:
[Matrix(C)*x C*x mul!(similar(x),C,x)]


Out[49]:
6×3 Array{Int64,2}:
 -94  -94  -94
  41   41   41
  -9   -9   -9
 120  120  120
 -31  -31  -31
 -43  -43  -43

In [50]:
# Fact 4 - Embedding Toeplitz() into Circulant()
n=5
a=rand(-6:6,2*n-1)
T=Toeplitz(a)


Out[50]:
5×5 Toeplitz{Int64}:
 -1  -6   5   1   3
 -6  -1  -6   5   1
  2  -6  -1  -6   5
 -5   2  -6  -1  -6
  1  -5   2  -6  -1

In [51]:
C=Circulant([a[n:2*n-1];a[1:n-1]])


Out[51]:
9×9 Circulant{Int64}:
 -1  -6   5   1   3   1  -5   2  -6
 -6  -1  -6   5   1   3   1  -5   2
  2  -6  -1  -6   5   1   3   1  -5
 -5   2  -6  -1  -6   5   1   3   1
  1  -5   2  -6  -1  -6   5   1   3
  3   1  -5   2  -6  -1  -6   5   1
  1   3   1  -5   2  -6  -1  -6   5
  5   1   3   1  -5   2  -6  -1  -6
 -6   5   1   3   1  -5   2  -6  -1

In [52]:
# Fact 5 - Toeplitz() x vector
x=rand(-6:6,n)


Out[52]:
5-element Array{Int64,1}:
 -2
  6
 -4
 -5
  4

In [53]:
[Matrix(T)*x T*x mul!(similar(x),T,x)]


Out[53]:
5×3 Array{Int64,2}:
 -47  -47  -47
   9    9    9
  14   14   14
  27   27   27
 -14  -14  -14

In [54]:
# Fact 6 - Hankel() x vector
H1=Hankel(a)


Out[54]:
5×5 Hankel{Int64}:
  3   1   5  -6  -1
  1   5  -6  -1  -6
  5  -6  -1  -6   2
 -6  -1  -6   2  -5
 -1  -6   2  -5   1

In [55]:
[Matrix(H1)*x H1*x mul!(similar(x),H1,x)]


Out[55]:
5×3 Array{Int64,2}:
   6    6    6
  33   33   33
  -4   -4   -4
   0    0    0
 -13  -13  -13

Example - Fast EVD of a Hankel matrix

Given a Hankel matrix $H$, the Lanczos method can be applied by defining a function (linear map) which returns the product $Hx$ for any vector $x$. This approach uses the package LinearMaps.jl and is described in the notebook Symmetric Eigenvalue Decomposition - Lanczos Method notebook.

The computation is very fast and allocates little extra space.


In [56]:
# import Pkg; Pkg.add("LinearMaps")

In [57]:
using LinearMaps

In [58]:
n=size(H,1)
f(x)=mul!(similar(x),H,x)


Out[58]:
f (generic function with 1 method)

In [59]:
H


Out[59]:
160×160 Hankel{Float64}:
  1.3104     0.115875  -1.08857   …   4.07448    3.35497    2.41421 
  0.115875  -1.08857   -2.22028       3.35497    2.41421    1.3104  
 -1.08857   -2.22028   -3.20152       2.41421    1.3104     0.115875
 -2.22028   -3.20152   -3.96587       1.3104     0.115875  -1.08857 
 -3.20152   -3.96587   -4.46374       0.115875  -1.08857   -2.22028 
 -3.96587   -4.46374   -4.66636   …  -1.08857   -2.22028   -3.20152 
 -4.46374   -4.66636   -4.56793      -2.22028   -3.20152   -3.96587 
 -4.66636   -4.56793   -4.18585      -3.20152   -3.96587   -4.46374 
 -4.56793   -4.18585   -3.55882      -3.96587   -4.46374   -4.66636 
 -4.18585   -3.55882   -2.74321      -4.46374   -4.66636   -4.56793 
 -3.55882   -2.74321   -1.80783   …  -4.66636   -4.56793   -4.18585 
 -2.74321   -1.80783   -0.827855     -4.56793   -4.18585   -3.55882 
 -1.80783   -0.827855   0.121836     -4.18585   -3.55882   -2.74321 
  ⋮                               ⋱                                 
  0.555961   1.35743    2.19081      -1.22175   -0.762656  -0.163073
  1.35743    2.19081    2.99615      -0.762656  -0.163073   0.555961
  2.19081    2.99615    3.70922   …  -0.163073   0.555961   1.35743 
  2.99615    3.70922    4.2674        0.555961   1.35743    2.19081 
  3.70922    4.2674     4.61573       1.35743    2.19081    2.99615 
  4.2674     4.61573    4.71235       2.19081    2.99615    3.70922 
  4.61573    4.71235    4.53309       2.99615    3.70922    4.2674  
  4.71235    4.53309    4.07448   …   3.70922    4.2674     4.61573 
  4.53309    4.07448    3.35497       4.2674     4.61573    4.71235 
  4.07448    3.35497    2.41421       4.61573    4.71235    4.53309 
  3.35497    2.41421    1.3104        4.71235    4.53309    4.07448 
  2.41421    1.3104     0.115875      4.53309    4.07448    3.35497 

In [60]:
A=LinearMap(f,n,issymmetric=true)


Out[60]:
LinearMaps.FunctionMap{Float64}(f, 160, 160; ismutating=false, issymmetric=true, ishermitian=true, isposdef=false)

In [61]:
size(A)


Out[61]:
(160, 160)

In [62]:
@time eigvals(Matrix(H));


  0.018234 seconds (178 allocations: 672.344 KiB, 64.00% gc time)

In [42]:
# import Pkg; Pkg.add("Arpack")

In [63]:
using Arpack

In [65]:
# Run twice
@time λA,UA=eigs(A, nev=6, which=:LM)


  0.002632 seconds (3.65 k allocations: 1.115 MiB)
Out[65]:
([-240.0, 240.0, -160.0, 160.0, 80.0, -80.0], [-0.0864252 -0.0709273 … -0.111459 0.00877199; -0.0986018 -0.0527038 … -0.108714 0.0261; … ; -0.0527038 -0.0986018 … -0.108714 -0.0261; -0.0709273 -0.0864252 … -0.111459 -0.00877199], 6, 1, 20, [5.92833e-15, 8.18108e-16, -2.91137e-15, 6.7392e-15, 7.61386e-15, 2.4128e-15, -3.39262e-15, -8.03346e-15, -3.09895e-15, 9.94065e-15  …  6.84203e-15, 1.0878e-14, -1.73401e-14, 4.81654e-15, 1.96373e-16, 1.0549e-14, 1.04138e-15, 1.43177e-14, 1.86044e-14, 1.18411e-14])

Extraction of non-stationary mono-components

Definitions

Let $x\in\mathbb{R}^{m}$, denote a signal with $N$ samples.

Assume $x$ is composed of $L$ non-stationary mono-components:

$$ x=\sum\limits_{k=1}^L x^{(k)}, $$

where

$$ x^{(k)}_i=A_k \cos(2\pi f_k i +\theta_k),\quad i=1,2,\ldots,m. $$

Assume that the normalized frequencies $f_k=\displaystyle\frac{F_k}{F}$, the sampling frequencies $F_k$, the amplitudes $A_k$, and the phases $\theta_k$, all sightly change in time.

Let $H\equiv H(x)$ be the Hankel matrix of $x$. The eigenpair of $(\lambda,u)$ of $H$ is significant if $|\lambda|> \tau \cdot \sigma(H)$. Here $\sigma(H)$ is the spectral radius of $H$, and $\tau$ is the significant threshold percentage chosen by the user depending on the type of the problem.

Fact

The following algorithm decomposes the signal $x$:

  1. Choose $\tau$ and form the Hankel matrix $H$
  2. Compute the EVD of $H$
  3. Choose the significant eigenpairs of $H$
  4. For each significant eigenpair $(\lambda,u)$
    1. Form the rank one matrix $M=\lambda uu^T$
    2. Define a new signal $y$ consisting of averages of the skew-diagonals of $M$
    3. Form the Hankel matrix $H(y)$
    4. Compute the EVD of $H(y)$
    5. Choose the significant eigenpairs of $H(y)$
    6. If $H(y)$ has only two significant eigenpairs, declare $y$ a mono-component, else go to step 4.

Example - Note A

Each tone has its fundamental frequency (mono-component). However, musical instruments produce different overtones (harmonics) which are near integer multiples of the fundamental frequency. Due to construction of resonant boxes, these frequencies slightly vary in time, and their amplitudes are contained in a time varying envelope.

Tones produces by musical instruments are nice examples of non-stationary signals. We shall decompose the note A4 played on piano.

For manipulation of recordings, we are using package WAV.jl. Another package with similar functionality is the package AudioIO.jl.


In [46]:
# import Pkg; Pkg.add("WAV")

In [66]:
using WAV

In [67]:
varinfo(WAV)


Out[67]:
name size summary
WAV 126.248 KiB Module
WAVArray 80 bytes UnionAll
WAVChunk 196 bytes DataType
WAVE_FORMAT_ALAW 2 bytes UInt16
WAVE_FORMAT_IEEE_FLOAT 2 bytes UInt16
WAVE_FORMAT_MULAW 2 bytes UInt16
WAVE_FORMAT_PCM 2 bytes UInt16
WAVFormat 236 bytes DataType
WAVFormatExtension 204 bytes DataType
WAVMarker 204 bytes DataType
bits_per_sample 0 bytes typeof(bits_per_sample)
isextensible 0 bytes typeof(isextensible)
isformat 0 bytes typeof(isformat)
wav_cue_read 0 bytes typeof(wav_cue_read)
wav_cue_write 0 bytes typeof(wav_cue_write)
wav_info_read 0 bytes typeof(wav_info_read)
wav_info_write 0 bytes typeof(wav_info_write)
wavappend 0 bytes typeof(wavappend)
wavplay 0 bytes typeof(wavplay)
wavread 0 bytes typeof(wavread)
wavwrite 0 bytes typeof(wavwrite)

In [68]:
# Load a signal
s, Fs = wavread("files/piano_A41.wav")


Out[68]:
([-0.0101321; -0.0102542; … ; 0.0; 0.0], 44100.0f0, 0x0010, WAVChunk[WAVChunk(Symbol("fmt "), UInt8[0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x44, 0xac, 0x00, 0x00, 0x88, 0x58, 0x01, 0x00, 0x02, 0x00, 0x10, 0x00])])

In [70]:
Fs


Out[70]:
44100.0f0

In [71]:
# Play the signal
wavplay(s,Fs)


┌ Warning: wavplay is not currently implemented on NT
└ @ WAV C:\Users\Ivan_Slapnicar\.julia\packages\WAV\uORV0\src\WAV.jl:26

In [72]:
# Plot the signal
plot(s)


Out[72]:
0 5.0×10 4 1.0×10 5 1.5×10 5 2.0×10 5 -1.0 -0.5 0.0 0.5 1.0 y1

In [73]:
# Plot in time scale
t=range(0,stop=length(s)/Fs,length=length(s))
plot(t,s,xlabel="Time")


Out[73]:
0 1 2 3 4 5 -1.0 -0.5 0.0 0.5 1.0 Time y1

In [74]:
# Total time and number of samples
t[end], length(s)


Out[74]:
(4.9821315f0, 219712)

In [75]:
# Detail
plot(s[1:800])


Out[75]:
0 200 400 600 800 -1.0 -0.5 0.0 0.5 1.0 y1

Let us visualize the signal in detail using the approach from the Julia is Fast notebook.


In [76]:
k=500
plot(collect(k:k+1000),s[k:k+1000])


Out[76]:
600 800 1000 1200 1400 -1.0 -0.5 0.0 0.5 1.0 y1

In [77]:
# Last part of the signal is just noise, so we read a 
# shorter signal. N must be odd.
sig = wavread("files/piano_A41.wav",100001)


Out[77]:
([-0.0101321; -0.0102542; … ; -0.00286874; -0.00305185], 44100.0f0, 0x0010, WAVChunk[WAVChunk(Symbol("fmt "), UInt8[0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x44, 0xac, 0x00, 0x00, 0x88, 0x58, 0x01, 0x00, 0x02, 0x00, 0x10, 0x00])])

In [78]:
typeof(sig)


Out[78]:
Tuple{Array{Float64,2},Float32,UInt16,Array{WAVChunk,1}}

In [79]:
s=sig[1]


Out[79]:
100001×1 Array{Float64,2}:
 -0.010132145146031068 
 -0.010254219183935057 
 -0.010254219183935057 
 -0.010132145146031068 
 -0.009949034089175085 
 -0.009643848994415113 
 -0.00933866389965514  
 -0.009094515823847163 
 -0.008789330729087191 
 -0.008606219672231208 
 -0.008423108615375225 
 -0.007995849482711264 
 -0.007202368236335337 
  ⋮                    
  0.003357036042359691 
  0.003173924985503708 
  0.002868739890743736 
  0.0023194067201757866
  0.0015259254737998596
  0.0005493331705679495
 -0.0004272591326639607
 -0.0013428144169438765
 -0.0021362956633198035
 -0.0026245918149357585
 -0.002868739890743736 
 -0.0030518509475997192

In [80]:
wavplay(s,Fs)


┌ Warning: wavplay is not currently implemented on NT
└ @ WAV C:\Users\Ivan_Slapnicar\.julia\packages\WAV\uORV0\src\WAV.jl:26

In [81]:
# File to play on Windows
wavwrite(sig[1],"files/piano_A41_short.wav",Fs=sig[2])

In [82]:
# Plot the short signal in time
t=range(0,stop=length(s)/Fs,length=length(s))
plot(t,s)


Out[82]:
0.0 0.5 1.0 1.5 2.0 -1.0 -0.5 0.0 0.5 1.0 y1

In [83]:
# Check the signal with FFT
# Notice 3 stronger harmonics and six more weaker ones
fs=abs.(fft(s))
plot(t,fs)


Out[83]:
0.0 0.5 1.0 1.5 2.0 0 1000 2000 3000 y1

In [84]:
# Details
l=10000
plot(t[1:l],fs[1:l])


Out[84]:
0.00 0.05 0.10 0.15 0.20 0 1000 2000 3000 y1

In [85]:
# Form the Hankel matrix
# IMPORTANT - Do not try to display H - it is a 50001 x 50001 matrix.
H=Hankel(vec(s));

In [86]:
size(H), H[100,200]


Out[86]:
((50001, 50001), 0.727927488021485)

In [87]:
@time fft(s);


  0.024813 seconds (65 allocations: 3.055 MiB)

In [88]:
# We are looking for 20 eigenvalue pairs
n=size(H,1)
f(x)=mul!(similar(x),H,x)
A=LinearMap(f,n,issymmetric=true)
size(A)


Out[88]:
(50001, 50001)

In [90]:
@time λ,U=eigs(A, nev=40, which=:LM)


  5.302702 seconds (24.05 k allocations: 1.652 GiB, 6.13% gc time)
Out[90]:
([1802.91, -1799.5, 1297.45, -1296.27, -537.888, 537.759, 416.906, -415.32, 410.179, -410.158  …  -94.8969, 94.8306, 92.442, -92.3572, 81.7541, -81.7477, 65.9095, -65.4746, 65.3807, -64.407], [-0.0147284 0.00759017 … -0.000934865 -0.0328479; -0.0151816 0.00665314 … 0.00407886 -0.0344853; … ; 0.00178267 0.00111078 … 0.000326988 -4.12385e-5; 0.00171032 0.00122072 … 0.000338463 -6.2934e-5], 40, 3, 123, [0.327824, 0.393072, 0.38077, 0.328561, 0.253591, 0.21197, 0.193901, 0.176152, 0.136156, 0.0567308  …  -0.0337112, -0.0446069, -0.0531296, -0.0578032, -0.0573832, -0.0517572, -0.0413592, -0.0275988, -0.0128235, -0.000654376])

In [91]:
# Count the eigenvalue pairs (+-) larger than the 10% of the maximum
τ=0.1
L=round(Int,(sum(abs.(λ).>(τ*maximum(abs,λ)))/2))


Out[91]:
11

At this point, the implementation using full matrices is rather obvious. However, we cannot do that, due to large dimension. Recall, the task is to define Hankel matrices $H_k$ for $k=1,\ldots,L$, from the signal obtained by averaging the skew-diagonals of the matrices

$$ H_k=\lambda_k U_{:,k}U_{:,k}^T + \lambda_{n-k+1} U_{:,n-k+1}U_{:,n-k+1}^T, $$

without actually forming the matrices.

This is a nice programming excercise which can be solved using $\cdot$ products.


In [93]:
function myaverages(λ::T, u::Vector{T}) where T
    n=length(u)
    x=Array{Float64}(undef,2*n-1)
    # Average lower diagonals
    for i=1:n
        x[i]=dot(u[1:i],reverse(u[1:i]))/i
    end
    for i=2:n
        x[n+i-1]=dot(u[i:n],reverse(u[i:n]))/(n-i+1)
    end
    λ*x
end


Out[93]:
myaverages (generic function with 1 method)

In [94]:
# A small test
u=[1,2,3,4,5]
u*u'


Out[94]:
5×5 Array{Int64,2}:
 1   2   3   4   5
 2   4   6   8  10
 3   6   9  12  15
 4   8  12  16  20
 5  10  15  20  25

In [96]:
myaverages(1,u)


Out[96]:
9-element Array{Float64,1}:
  1.0               
  2.0               
  3.3333333333333335
  5.0               
  7.0               
 11.0               
 15.333333333333334 
 20.0               
 25.0               

We now execute the first step of the algorithm from the above Fact.

Notice that eigs() returns the eigenvalues arranged by the absoulte value, so the consecutive pairs define the $i$-th signal. The computation of averages is long - it requires $O(n^2)$ operations and takes several minutes.


In [97]:
# This step takes 7 minutes, so we skip it

# xcomp=Array(Array{Float64},L)
# for k=1:L
#     xcomp[k]=myaverages(λ[2*k-1],U[:,2*k-1])+myaverages(λ[2*k],U[:,2*k])
# end

Can we do without averaging?

The function myaverages() is very slow - 7 minutes, compared to 5 seconds for the eigenvalue computation.

The simplest option is to disregard the averages, and use the first column and the last row of the underlying matrix, as in definition of Hankel matrices, which we do next. Smarter approach might be to use small random samples to compute the averages.

Let us try the simple approach for the fundamental frequency. (See also the notebook Examples in Signal Decomposition.ipynb.)


In [98]:
xcomp=Array{Array{Float64}}(undef,L)
for k=1:L
    k1=2*k-1
    k2=2*k
    xsimple=[(λ[k1]*U[1,k1])*U[:,k1]; (λ[k1]*U[n,k1])*U[2:n,k1]]
    xsimple+=[(λ[k2]*U[1,k2])*U[:,k2]; (λ[k2]*U[n,k2])*U[2:n,k2]]
    xcomp[k]=xsimple
end

Let us look and listen to what we got:


In [99]:
typeof(xcomp[1])


Out[99]:
Array{Float64,1}

In [112]:
k=4
plot(t,xcomp[k])


Out[112]:
0.0 0.5 1.0 1.5 2.0 -0.3 -0.2 -0.1 0.0 0.1 0.2 0.3 y1

In [114]:
k=4
plot(t[1:1000],xcomp[k][1:1000])


Out[114]:
0.000 0.005 0.010 0.015 0.020 -0.3 -0.2 -0.1 0.0 0.1 0.2 0.3 y1

In [127]:
# Plot short first parts of FFTs of and display frequency
l=10000
k=11
fs=abs.(fft(xcomp[k]))
m,ind=findmax(fs[1:l])
println("Frequency = ", ind*Fs/length(fs)  ," Hz, Amplitude = ", m)
plot(t[1:l],fs[1:l])


Frequency = 2645.0916 Hz, Amplitude = 221.78049080111802
Out[127]:
0.00 0.05 0.10 0.15 0.20 0 50 100 150 200 y1

We see that all xcomp[k] are clean mono-components - see Physics of Music - Notes:

1 = 440 Hz (A4)
2 = 880 Hz (2*440,+octave,A5)
3 = 1320 Hz (3*440,+quint,E6)
4 = 440 Hz  
5 = 880 Hz
6 = 2200 Hz (5*440,++major terza, C#7) 
7 = 2640 Hz (6*440,++quint,E7)
8 = 440 Hz
9 = 2200 Hz
10 = 1760 Hz (4*440,++octave,A6)
11 = 2640 Hz

N.B. Some mono-components are repeated, and they should be grouped by adding components with absolute weighted correlation larger than some prescribed threshold.


In [77]:
wavplay(xcomp[2],Fs)

In [78]:
wavplay(sum([xcomp[i] for i=1:11]),Fs)

In [128]:
# On Windows, store the mono-components
for i=1:11
    wavwrite(xcomp[i],"files/comp$i.wav",Fs=sig[2])
end

In [ ]: